昨天我們已經學習了如何客製化 Serializer 來調整回傳給前端與前端傳入的格式了,今天讓我們來學習如何調整 ViewSet,讓他使用不同的序列化吧!
目前我們 API 的做法是讓前端在新增時給 is_finish
的值,如果要調整是否完成則可以透過 PUT
或 PATCH
來處理。但有時候我們會希望使用者再建立任務時不能建立已經完成的任務(也就是 is_finish
為 True)的任務,且 is_finish
這個欄位依然要可以透過 PUT 或 PATCH 的 API 更新。
所以我們今天需要完成的調整有兩項
現在就讓我們開始吧!
這個需求是需要讓建立的 API 不能調整調整 is_finish 這個欄位,根據我們前面學習到的如果我們希望調整傳入或是傳出的格式,要調整的是 Serializer,所以讓我們打開 server/app/todo/serializers.py
並依照下方方式調整內容
# ...... 以上省略 ......
class TaskSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, read_only=True)
tag_ids = serializers.PrimaryKeyRelatedField(
allow_empty=False,
write_only=True,
many=True,
queryset=todo_models.Tag.objects.all(),
source="tags",
)
class Meta:
model = todo_models.Task
- fields = "__all__"
+ exclude = ("is_finish",)
這邊我們將 TaskSerializer 中 Meta 的 fields 替換成 exclude,並設定成 ("is_finish",)
讓我們看看這段修改了些什麼,本來我們是透過 fields 設定這個序列化要顯示哪些欄位,本來我們設定成 __all__
代表的是我們要顯示 Task Model 中的所有欄位,其實也可以設定成類似像這樣 ("field1", "field2")
代表的是這個序列化中只顯示 Model 中的 field1 與 field2。那 exclude 其實與 fields 類似,指示他是反向表述表達說在這個序列化中我們「不」要顯示什麼欄位,像我們現在的設定代表的是顯示 Task Model 中的除了 is_finish 以外的所有欄位。
現在大家可以利用 Postman 測試看看,讓我們呼叫 POST http://127.0.0.1:8000/api/todo/tasks 並設定 HTTP Body 為
{
"title": "測試測試",
"description": "這是一個測試任務",
"is_finish": true,
"tag_ids": [
1
]
}
P.S. 別忘了先啟動虛擬環境與 server 唷
現在我們可以看到回傳值如下
{
"id": 16,
"tags": [
{
"id": 1,
"name": "T01"
}
],
"title": "測試測試",
"description": "這是一個測試任務"
}
我們會發現 is_finish 完全不見了,但我們可以在 DB 管理工具中看到 is_finish 欄位是 False,為什麼會這樣呢?
是因為我們在序列化中設定了不接受 is_finish 的傳入與傳出,所以雖然我們在 Body 中有傳入 is_finish 但他會被序列化過濾掉,所以資料庫中存的會是欄位的預設值也就是 False,而因為我們將 is_finish 欄位整個排除掉了造成回傳值也沒有出現這個欄位。
這時候雖然我們完成需求了,但是前端這時候就不開心了,他們拿不到 is_finish 的值了,所以要讓我們再調整一下吧。
讓我們打開 server/app/todo/serializers.py
並依照下方方式調整內容
# ...... 以上省略 ......
class TaskSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, read_only=True)
tag_ids = serializers.PrimaryKeyRelatedField(
allow_empty=False,
write_only=True,
many=True,
queryset=todo_models.Tag.objects.all(),
source="tags",
)
class Meta:
model = todo_models.Task
- exclude = ("is_finish",)
+ fields = "__all__"
+ read_only_fields = ("is_finish",)
P.S. 我知道 fields 又被加回來了,但前面是因為想要介紹 exclude 給大家
修改成這樣可以達到的效果是 TaskSerializer 還是會顯示 Task 中的所有欄位,但是因為我們有設定 read_only_fields
所以 is_finish 欄位會被設定成只能讀取。
現在再讓我們呼叫一次 POST http://127.0.0.1:8000/api/todo/tasks 且使用 HTTP Body
{
"title": "測試任務與標籤",
"description": "這是一個測試任務",
"is_finish": true,
"tag_ids": [
1
]
}
會看到回傳值變成
{
"id": 20,
"tags": [
{
"id": 1,
"name": "T01"
}
],
"title": "測試任務與標籤",
"description": "這是一個測試任務",
"is_finish": false
}
這樣我們就拿到目的了,傳入時無法修改 is_finish 但是回傳時會正常顯示,大家也可以使用 PUT 或 PATCH 測試一下更新,目前效果也會是相同的。
現在因為我們調整了序列化讓 is_finish 欄位都無法更改了,所以現在讓我們來調整一下讓更新相關的 API 變成能修改 is_finish 欄位吧。
因為一樣又牽涉到欄位問題,所以讓我們繼續打開 server/app/todo/serializers.py
並依照下方方式調整內容
# ...... 以上省略 ......
class TaskSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, read_only=True)
tag_ids = serializers.PrimaryKeyRelatedField(
allow_empty=False,
write_only=True,
many=True,
queryset=todo_models.Tag.objects.all(),
source="tags",
)
class Meta:
model = todo_models.Task
fields = "__all__"
- read_only_fields = ("is_finish",)
+class TaskCreateSerializer(TaskSerializer):
+ class Meta(TaskSerializer.Meta):
+ read_only_fields = ("is_finish",)
依照我們的需求,除了新增的 API 以外其他都需要可以修改 is_finish 欄位,只有新增 Task 的時候需要「額外」將 is_finish 設定為唯讀的。
所以我們先將 TaskSerializer 的 read_only_fields 設定刪除,讓他的功能恢復到之前的樣子,並且在下方建立一個新的序列化名為 TaskCreateSerializer 他繼承了 TaskSerializer 所以他會擁有 TaskSerializer 的所有的功能,唯一的不同是需要將 is_finish 設定為唯讀的,所以在 TaskCreateSerializer 的 Meta 中額外加上 read_only_fields 設定。
這樣就可以做到當使用 TaskSerializer 可以修改 is_finish 使用 TaskCreateSerializer 時不能修改 is_finish 欄位,所以現在我們只需要設定 ViewSet 讓他在新增時使用 TaskCreateSerializer 其他時候使用 TaskSerializer。
打開 server/app/todo/views.py
並依照下方修改檔案內容
# ...... 以上省略 ......
class TaskViewSet(viewsets.ModelViewSet):
queryset = todo_models.Task.objects.all()
serializer_class = todo_serializers.TaskSerializer
+ def get_serializer_class(self):
+ if self.action == "create":
+ return todo_serializers.TaskCreateSerializer
+
+ return super().get_serializer_class()
# ...... 以下省略 ......
這邊我們透過覆寫 get_serializer_class 並且判斷,當 action 為 create 時我們要回傳 TaskCreateSerializer 讓 ViewSet 使用,其他狀況直接回傳 super().get_serializer_class()
(會回傳 serializer_class 設定的序列化),現在大家可打開 Postman 測試看看,應該就會發現功能已經變成新增不能指定 is_finish 但是更新可以囉。
這邊我們也為大家整理下各個動作的 action
也可以參考官方文件
今天我們透過在 ViewSet 控制要回傳哪個序列化讓不同的動作可以有不同的傳入傳出。結束前別忘了檢查一下今天的程式碼有沒有問題,並排版好喔。
ruff check --fix .
black .
pyright .
明天讓我們看看前端會提出什麼更奇怪的需求讓我們完整任務清單吧!
P.S. 今天的檔案更新可以參考我的 Git Commit 大家可以搭配服用